// Learn TypeScript:
//  - https://docs.cocos.com/creator/2.4/manual/en/scripting/typescript.html
// Learn Attribute:
//  - https://docs.cocos.com/creator/2.4/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
//  - https://docs.cocos.com/creator/2.4/manual/en/scripting/life-cycle-callbacks.html

const { ccclass, property } = cc._decorator;

enum Direction {
    NONE,
    LEFT,
    RIGHT,
    TOP,
    BOTTOM
}

enum TileType {
    TILE,
    BORDER_TOP_TILE,
    BORDER_BOTTOM_TILE,
    BORDER_LEFT_TILE,
    BORDER_RIGHT_TILE,
}

const _v1 = cc.Vec2.ZERO;

@ccclass
export default class TiledMap extends cc.Component {
    @property({
        displayName: "上边界",
    })
    borderTop: number = 0;

    @property({
        displayName: "下边界",
    })
    borderBottom: number = 0;

    @property({
        displayName: "左边界",
    })
    borderLeft: number = 0;

    @property({
        displayName: "右边界",
    })
    borderRight: number = 0;

    @property({
        displayName: "是否限制上边界"
    })
    limitTop: boolean = true;

    @property({
        displayName: "是否限制下边界"
    })
    limitBottom: boolean = true;

    @property({
        displayName: "是否限制左边界"
    })
    limitLeft: boolean = true;

    @property({
        displayName: "是否限制右边界"
    })
    limitRight: boolean = true;

    @property({
        displayName: "上外边框填充节点",
        type: cc.Node
    })
    borderTopNodes: cc.Node[] = [];

    @property({
        displayName: "下外边框填充节点",
        type: cc.Node
    })
    borderBottomNodes: cc.Node[] = [];

    @property({
        displayName: "左外边框填充节点",
        type: cc.Node
    })
    borderLeftNodes: cc.Node[] = [];

    @property({
        displayName: "右外边框填充节点",
        type: cc.Node
    })
    borderRightNodes: cc.Node[] = [];
    // /**
    //  * 重复尺寸
    //  */
    // private _repeatSize: cc.Size = cc.Size.ZERO;
    /** 
     * 屏幕重复尺寸
     */
    private _screenRepeatSize: cc.Size = cc.Size.ZERO;

    private _activeTNodes: Array<Array<cc.Node>> = [];
    private _activeBTNodes: Array<Array<cc.Node>> = [];
    private _activeBBNodes: Array<Array<cc.Node>> = [];
    private _activeBLNodes: Array<Array<cc.Node>> = [];
    private _activeBRNodes: Array<Array<cc.Node>> = [];

    private _viewPos: cc.Vec2 = cc.Vec2.ZERO;
    private _viewSize: cc.Size = cc.winSize;

    private _tileCacheMap: Map<number, Array<cc.Node>> = new Map();
    private _borderTopCacheMap: Map<number, Array<cc.Node>> = new Map();
    private _borderBottomCacheMap: Map<number, Array<cc.Node>> = new Map();
    private _borderLeftCacheMap: Map<number, Array<cc.Node>> = new Map();
    private _borderRightCacheMap: Map<number, Array<cc.Node>> = new Map();

    private _tileUnit: Array<Array<cc.Node>> = [];
    private _borderTopUnit: Array<Array<cc.Node>> = [];
    private _borderBottomUnit: Array<Array<cc.Node>> = [];
    private _borderLeftUnit: Array<Array<cc.Node>> = [];
    private _borderRightUnit: Array<Array<cc.Node>> = [];

    onLoad() {
        if (this.node.children.length) {
            let children = this.node.children;
            let tileSize = children[0].getContentSize();
            let repeatSize = cc.Size.ZERO;
            for (let i = 0, len = children.length; i < len; i++) {
                repeatSize.width = Math.max(repeatSize.width,
                    Math.floor(children[i].x / tileSize.width) + 1);
                repeatSize.height = Math.max(repeatSize.height,
                    Math.floor(children[i].y / tileSize.height) + 1);
            }

            let x, y, id;
            for (let i = 0, len = children.length; i < len; i++) {
                x = Math.floor(children[i].x / tileSize.width);
                y = Math.floor(children[i].y / tileSize.height);
                id = y * tileSize.height + x;
            }
            this._screenRepeatSize.width = Math.ceil(cc.winSize.width / tileSize.width) + 1;
            this._screenRepeatSize.height = Math.ceil(cc.winSize.height / tileSize.height + 1);

            this.createTiles(this._activeTNodes, this._tileUnit,
                new cc.Vec4(this._screenRepeatSize.height * tileSize.height, 0,
                    0, this._screenRepeatSize.width * tileSize.width),
                TileType.TILE);
        }

        //初始化边界
        let repeatX, repeatY, tile: cc.Node;
        if (this.limitTop && this._borderTopUnit.length) {
            tile = this._borderTopUnit[0][0];
            repeatX = Math.ceil(cc.winSize.width / tile.width) + 1;
            repeatY = Math.ceil(cc.winSize.height / tile.height + 1);
            this.createTiles(this._activeBTNodes, this._borderTopUnit,
                new cc.Vec4(repeatY * tile.height, 0, 0, repeatX * tile.width), TileType.BORDER_TOP_TILE);
        }

        if (this.limitBottom) {
            this.createTiles(this.borderBottomNodes, this._activeBBNodes, this.borderBottom, BorderType.BOTTOM);
        }

        if (this.limitLeft) {
            this.createTiles(this.borderLeftNodes, this._activeBLNodes, this.borderLeft, BorderType.LEFT);
        }

        if (this.limitRight) {
            this.createTiles(this.borderRightNodes, this._activeBRNodes, this.borderRight, BorderType.RIGHT);
        }
    }

    public setViewSize(viewSize: cc.Size) {

    }

    public updateViewPosition(viewWorldPos: cc.Vec2) {
        // let localPos = this.node.convertToNodeSpaceAR(cameraWorldPos);
        // let hW = cc.winSize.width * 0.5;
        // let hH = cc.winSize.height * 0.5;
        // let cameraBorderTop = localPos.y + hH;
        // let cameraBorderBottom = localPos.y - hH;
        // let cameraBorderLeft = localPos.x - hW;
        // let cameraBorderRight = localPos.x + hW;
        //更新地块

        //更新边界
        // let cameraBorder = new cc.Vec4(cameraBorderTop, cameraBorderBottom, cameraBorderLeft, cameraBorderRight);
        // this.updateBorder(cameraBorder, this._activeBTNodes, BorderType.TOP);
        // this.updateBorder(cameraBorder, this._activeBBNodes, BorderType.BOTTOM);
        // this.updateBorder(cameraBorder, this._activeBLNodes, BorderType.LEFT);
        // this.updateBorder(cameraBorder, this._activeBRNodes, BorderType.RIGHT);
    }

    private createTiles(
        activeNodes: Array<Array<cc.Node>>,
        units: Array<Array<cc.Node>>,
        limitBorder: cc.Vec4,
        tileType: TileType
    ) {

        let limitTop = limitBorder.x, limitBottom = limitBorder.y,
            limitLeft = limitBorder.z, limitRight = limitBorder.w;

        let tile = units[0][0], temp: cc.Node;
        let xRepeats = (limitRight - limitLeft) / tile.width;
        let yRepeats = (limitTop - limitBottom) / tile.height;
        let unitWidth = units[0].length, unitHeight = units.length;
        let unitX, unitY;

        for (let i = 0; i < yRepeats; i++) {
            let rows = activeNodes[i] = [];
            unitY = i % unitHeight;
            for (let j = 0; j < xRepeats; j++) {
                unitX = j % unitWidth;
                temp = this.loadTile(unitX, unitY, tileType);
                temp.parent = this.node;
                temp.setPosition(j * tile.width + limitLeft, i * tile.height + limitBottom);
                rows.push(temp);
            }
        }
    }

    private updateTiles(
        activeTiles: Array<Array<cc.Node>>,
        tilesBorder: cc.Vec4,
        unitsSize: cc.Size,
        viewPos: cc.Vec2,
        limitBorder: cc.Vec4,
        tileType: TileType
    ) {
        if (viewPos.equals(this._viewPos) || !activeTiles.length || !activeTiles[0].length) return;

        let hDirection = viewPos.x > this._viewPos.x ? Direction.RIGHT : Direction.LEFT;
        let vDirection = viewPos.y > this._viewPos.y ? Direction.TOP : Direction.BOTTOM;

        let hViewHeight = 0.5 * this._viewSize.height, hViewWidth = 0.5 * this._viewSize.width;

        let viewTop = viewPos.y + hViewHeight, viewBottom = viewPos.y - hViewWidth,
            viewLeft = viewPos.x - hViewWidth, viewRight = viewPos.x + hViewWidth;

        let limitTop = limitBorder.x, limitBottom = limitBorder.y,
            limitLeft = limitBorder.z, limitRight = limitBorder.w;

        let tilesTop = tilesBorder.x, tilesBottom = tilesBorder.y,
            tilesLeft = tilesBorder.z, tilesRight = tilesBorder.w;

        let tileCols = activeTiles[0].length;
        let tileRows = activeTiles.length;
        let tile = activeTiles[0][0];
        let repeatStartUnitIndex = 0;
        let repeatIndex = 0;
        //水平方向
        let limit = (hDirection === Direction.LEFT && tilesLeft <= limitLeft) ||
            (hDirection === Direction.RIGHT && tilesRight >= limitRight);

        if (!limit) {
            //将tile整体向左/右移动
            let repeatStartX = 0;

            let unitLength = unitsSize.width * tile.width;
            let s = hDirection === Direction.LEFT ? -1 : 1;

            if (hDirection === Direction.LEFT) {
                repeatStartX = Math.min(tilesLeft - tile.width, viewRight);
            } else {
                repeatStartX = Math.max(tilesRight + tile.width, viewLeft);
            }

            let t = repeatStartX % unitLength;
            repeatStartUnitIndex = Math.floor((t >= 0 ? t : t + unitLength) / tile.width);

            repeatIndex = Math.floor(repeatStartX / unitLength);

            for (let i = 0; i < tileCols; i++) {
                tile = activeTiles[0][i];
                if (tile.x > viewRight || tile.x + tile.width < viewLeft) continue;
                let colUnitIndex = Math.floor((Math.abs(tile.x) % unitLength) / tile.width);
                for (let j = 0; j < tileRows; j++) {
                    if (colUnitIndex !== repeatStartUnitIndex) {
                        this.getTileUnitRowCol(activeTiles[j][i].getPosition(), activeTiles[j][i].getContentSize(), unitsSize, _v1);
                        this.unloadTile(activeTiles[j][i], _v1.x, _v1.y, tileType);
                        activeTiles[j][i] = this.loadTile(_v1.x, _v1.y, tileType);
                    }
                    activeTiles[j][i].x = repeatIndex * unitLength + repeatStartUnitIndex * tile.width;
                }

                if (repeatStartUnitIndex + s >= unitsSize.width || repeatStartUnitIndex + s < 0) {
                    repeatIndex += s;
                }
                repeatStartUnitIndex = (repeatStartUnitIndex + unitsSize.width + s) % unitsSize.width;

                //更新tiles左右边界
                tilesBorder.z = Math.min(tilesBorder.z, activeTiles[0][i].x);
                tilesBorder.w = Math.max(tilesBorder.w, activeTiles[0][i].x);
            }
        }
        //垂直方向
        limit = (vDirection === Direction.TOP && tilesTop >= limitTop) ||
            (vDirection === Direction.BOTTOM && tilesBottom <= limitBottom);
        if (!limit) {
            let repeatStartY = 0;
            let unitLength = unitsSize.height * tile.height;
            let s = vDirection === Direction.BOTTOM ? -1 : 1;

            if (vDirection === Direction.BOTTOM) {
                repeatStartY = Math.min(tilesBottom - tile.height, viewTop);
            } else {
                repeatStartY = Math.max(tilesTop + tile.height, viewBottom);
            }
            let t = repeatStartY % unitLength;
            repeatStartUnitIndex = Math.floor((t >= 0 ? t : t + unitLength) / tile.height);
            repeatIndex = Math.floor(repeatStartY / unitLength);

            for (let i = 0; i < tileRows; i++) {
                tile = activeTiles[i][0];
                if (tile.y > viewTop || tile.y + tile.height < viewBottom) continue;
                let rowUnitIndex = Math.floor((Math.abs(tile.y) % unitLength) / tile.height);
                for (let j = 0; j < tileCols; j++) {
                    if (rowUnitIndex !== repeatStartUnitIndex) {
                        this.getTileUnitRowCol(activeTiles[i][j].getPosition(), activeTiles[i][j].getContentSize(), unitsSize, _v1);
                        this.unloadTile(activeTiles[i][j], _v1.x, _v1.y, tileType);
                        activeTiles[i][j] = this.loadTile(_v1.x, _v1.y, tileType);
                    }
                    activeTiles[i][j].y = repeatIndex * unitLength + repeatStartUnitIndex * tile.height;
                }

                if (repeatStartUnitIndex + s >= unitsSize.height || repeatStartUnitIndex + s < 0) {
                    repeatIndex += s;
                }
                repeatStartUnitIndex = (repeatStartUnitIndex + unitsSize.height + s) % unitsSize.height;

                //更新tiles上下边界
                tilesBorder.y = Math.min(tilesBorder.y, activeTiles[i][0].y);
                tilesBorder.x = Math.max(tilesBorder.x, activeTiles[i][0].y);
            }
        }
    }

    private loadTile(x: number, y: number, tileType: TileType) {
        let cacheMap = this.getCacheMap(tileType);
        let unit = this.getTileUnit(tileType);
        let index = y * unit.length + x;
        let tiles = cacheMap.get(index);
        let tile: cc.Node;
        if (!tiles || !tiles.length) {
            tile = cc.instantiate(unit[y][x]);
        } else {
            tile = tiles.pop();
        }
        return tile;
    }

    private unloadTile(tile: cc.Node, x: number, y: number, tileType: TileType) {
        tile.active = false;
        let cacheMap = this.getCacheMap(tileType);
        let unit = this.getTileUnit(tileType);
        let index = y * unit.length + x;
        let tiles = cacheMap.get(index);
        if (!tiles) {
            cacheMap.set(index, tiles = []);
        }
        tiles.push(tile);
    }

    private getTileUnitRowCol(tilePos: cc.Vec2, tileSize: cc.Size, unitSize: cc.Size, out: cc.Vec2) {
        let unitWidth = unitSize.width * tileSize.width;
        let unitHeight = unitSize.height * tileSize.height;
        let t = tilePos.x % unitWidth;
        out.x = Math.floor((t >= 0 ? t : t + unitWidth) / tileSize.width);
        t = tilePos.y % unitHeight;
        out.y = Math.floor((t >= 0 ? t : t + unitWidth) / tileSize.height);
    }

    private getCacheMap(tileType: TileType) {
        let map: Map<number, cc.Node[]>;
        switch (tileType) {
            case TileType.TILE: map = this._tileCacheMap; break;
            case TileType.BORDER_TOP_TILE: map = this._borderTopCacheMap; break;
            case TileType.BORDER_BOTTOM_TILE: map = this._borderBottomCacheMap; break;
            case TileType.BORDER_LEFT_TILE: map = this._borderLeftCacheMap; break;
            case TileType.BORDER_RIGHT_TILE: map = this._borderRightCacheMap; break;
        }
        return map;
    }

    private getTileUnit(tileType: TileType) {
        let unit: Array<cc.Node[]>;
        switch (tileType) {
            case TileType.TILE: unit = this._tileUnit; break;
            case TileType.BORDER_TOP_TILE: unit = this._borderTopUnit; break;
            case TileType.BORDER_BOTTOM_TILE: unit = this._borderBottomUnit; break;
            case TileType.BORDER_LEFT_TILE: unit = this._borderLeftUnit; break;
            case TileType.BORDER_RIGHT_TILE: unit = this._borderRightUnit; break;
        }
        return unit;
    }

    protected onDestroy(): void {

    }
}
